Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@starbeam/timeline

Package Overview
Dependencies
Maintainers
2
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@starbeam/timeline

`@starbeam/timeline` is part of Starbeam, a library for building and using reactive objects in any framework.

  • 0.8.9
  • latest
  • npm
  • Socket score

Version published
Maintainers
2
Created
Source

Purpose

@starbeam/timeline is part of Starbeam, a library for building and using reactive objects in any framework.

Primitive

@starbeam/timeline is stable, with the same semver policy as Starbeam.

That said, it is not intended to be used directly by application code. Rather, it is one of the core parts of the Starbeam composition story. You can use it to better understand how Starbeam works, or to build your own Starbeam libraries.

📙 Philosophy

Higher-level libraries like @starbeam/universal build on lower-level primitives. These are not privileged internal APIs, and they are not marked as unstable. We believe that you, the people building Starbeam's library ecosystem, are just as innovative as Starbeam's creators. We avoid including "for me but not for thee" APIs in our composition abstractions. Go forth and build!

Timeline and Lifetime

At a fundamental level, Starbeam reactivity is made up of mutation events that happen to a data universe at a point on a timeline.

The data universe is broken up into two kinds of cells: data cells and formulas.

Data Cells
A single, atomic piece of mutable data.
Formulas
Computations that derive data from other data cells or formulas.

Two Phases: Action and Render

The Starbeam reactivity system is a perpetual cycle between two phases: Action and Render. These phases run in a cycle for as long as the program is running.

The Action Phase
Application code freely mutates the data universe.
The Render Phase
The data universe is reflected onto the rendered output.

The Action Phase

Code in the Action phase is quite powerful. It can mutate data cells as much as it wants, and it can immediately get the up-to-date values of formulas. Code in the Action phase can also read from the rendered output.

In exchange for all of that power, code in the Action phase cannot directly write to the rendered output, and it will need to wait until the next Action phase to see how the mutations to the data universe reflected onto the rendered output.

The Render Phase

Code in the Render phase is considerably less powerful. It may read from the data universe and write to the rendered output, but it may not write to the data universe.

ℹ️ The Complete Rendering Process

When using Starbeam to reflect the data universe into a Browser DOM, rendering involves multiple iterations of the Action / Render cycle. We call the entirety of this process the Rendering Process.

The Rendering Process
The cycles of Action / Render

These steps allow you to implement framework-agnostic [resources] that can correctly use the DOM as data source. They are universal, which means that you can write code in terms of Starbeam's APIs, and it will run inside of the framework of your choice with a Starbeam adapter.

Step 1: Initial Render
Set up your data universe and compute your framework's representation of HTML for the first time.
First Render Phase: DOM Insertion
Your framework is inserting your HTML into the DOM. From now until it has painted, which occurs after Step 2, the browser will not accept hardware events from the user.
Step 2: Measurement
The HTML you produced is now in the DOM, but the browser has not yet painted it. You can safely read measurements and styles from the DOM and write that information into the data universe. This phase allows you to participate in the layout and styling process in a relatively efficient manner.
Second Render Phase: User-aided layout
Step 3: Ready
The browser has painted your component, which includes the changes you made to the data universe in the measurement step. The browser is now idle, and is accepting user events again. Now is a great time to perform steps that aren't critical to the layout or styling of your component, such as kicking off asynchronous queries.

📒 Note

Each of these three steps is an Action / Render cycle. During the Action phase, application code can mutate the data universe and read from the DOM. During the Render phase, your framework will update the DOM from the changes you made during the Action phase. Typically, the three cycles of the rendering phase happen in quick succession, but you should not rely on this. Your framework may choose to do other work between the phases, and modern frameworks commonly do so in order to provide an optimal experience for your users.

Also, while application code will typically have an opportunity to run inside of each step of the Rendering Process, your framework may choose to [deactivate] or [unmount] the component before the Ready step. If application code sets up some state that needs to be torn down, it should not rely on the Measurement or Ready steps running. Instead, finalizers registered with an appropriate lifetime (see below) are guaranteed to run even if the Measurement or Ready steps do not.

In practice, these considerations are bundled together into the high-level "Stateful Formula" construct provided by @starbeam/reactive.

Timeline

The Timeline in @starbeam/timeline coordinates these phases.

It starts out in the Actions phase, which allows free access to the data universe. As soon as a data cell in the data universe is mutated, the Timeline schedules a Render phase using the configured scheduler. By default, this will schedule a Render phase during a microtask checkpoint, which occurs asynchronously, but before the next paint.

Scheduling

The Timeline can be configured with a Coordinator (see @starbeam/schedule), which controls the exact details of the timeline's timing.

The default behavior automatically schedules the next Render phase using a microtask checkpoint, which means that it will happen asynchronously, but before the next time the browser paints the page. The purpose of the Coordinator is to allow you to make multiple mutations to the data universe before a Render phase occurs.

You can specify a custom Coordinator to use an alternative strategy. For example, if you are writing a single-file demo, the entire file will finish running before a microtask checkpoint. You could create an API to use a single-file demo that automatically schedules Render phases at appropriate times.

Finally, you can also explicitly schedule a Render phase, which will supersede the Coordinator's policy and simply wait until you're ready to render.

import { TIMELINE } from "@starbeam/timeline";
import { Cell } from "@starbeam/reactive";

const person = reactive({
  id: null,
  name: "@tomdale",
});
const name = Cell("Tom");
const userId = Cell(null);

function multiStepProcess(name, url) {
  const render = TIMELINE.manualRenderPhase();

  person.name = name;

  fetch(url).then((data) => {
    person.id = data.id;
    render();
  });
}

Moving Along the Timeline

The Timeline is a representation of discrete time, where each mutation to a data cell is given a unique, monotonically increasing timestamp.

😵‍💫 Here's what that means:

Every time you mutate a data cell, the Timeline assigns increments the "current timestamp" by 1, and assigns that timestamp to the mutation.

"Discrete time" just means that there are specific points in time that we are interested in, and that "nothing interesting" happens in between those points.

When you are in an Action phase, this happens automatically.

On the other hand, Render phases are frozen in time. They may not move the timeline forward. In practice, this means that formulas are read-only and may not mutate the data universe.

💡 Note

This has nothing to do with the location of callbacks in the code. For example, it is quite normal for event handlers to occur inside initialization (the code that computes the initial state of the DOM from the data universe). However, the event handlers do not run during initialization, but rather at a later time, in response to hardware events triggered by the user.

By definition, such events happen in the Action phase, even though the function that they call was created during initialization.

Formula Validation

TODO: Describe the validation process

Subscription

TODO: Describe how to subscribe to changes in a formula

Structured Finalizer

As we discussed, the timeline describes changes in the data universe and helps a consumer coordinate the two-phase process of reflecting the data universe onto the output. Both data cells and formulas are pure data: they can be automatically cleaned up by the garbage collector when nobody retains a reference to them.

On the other hand, you may encounter objects in the real world that require you to tear them down when you're done using them, and you may want to convert those objects into data in the data universe. That's where the structured finalizer comes in.

The structured finalizer allows you to set up a stateful connection to some external data, such as a WebSocket, ResizeObserver or even a fetch request, associate it with an owner, and automatically finalize the connection when the owner is finalized.

For example, a component may set up a ResizeObserver to keep track of the size of one of the elements it creates. When the component is deactivated, the component wants to finalize the ResizeObserver so that it doesn't leak.

Composition is King

Starbeam uses the structured finalizer approach to make finalization composable. Instead of making the component responsible for setting up the ResizeObserver and specifying how to finalize the ResizeObserver when the component it finalized, it can delegate responsibility to an ElementSize resource:

  1. Create an object that represents the ResizeObserver.
  2. Convert events on the ResizeObserver into data in the data universe.
  3. Specify what should happen when that object is finalized.
  4. Link that object to the component.

Example: The Resource Pattern

Let's see how this all fits together. We'll use the resource pattern from @starbeam/reactive to create an ElementSize resource.

import { Resource } from "@starbeam/reactive";

export function ElementSize(element: Element) {
  return Resource((resource) => {
    const size = reactive(getSize(element));

    const observer = new ResizeObserver();

    observer.observe(element, () => {
      const { width, height } = getSize(element);

      size.width = width;
      size.height = height;
    });

    resource.on.finalize(() => observer.disconnect());

    return size;
  });
}

function getSize(element: Element) {
  const rect = element.getBoundingClientRect();

  return {
    width: rect.width,
    height: rect.height,
  };
}

Let's look at it one piece at a time.

First, we create a vanilla getSize function to get the width and heigh from an element.

function getSize(element: Element) {
  const rect = element.getBoundingClientRect();

  return {
    width: rect.width,
    height: rect.height,
  };
}

Next, we create a function that will take an element and set up the resource.

export function ElementSize(element: Element) {
  return Resource((resource) => {
    // ...
  });
}

This function operates on a fixed element, such as the top-level element of a component. The function calls the Resource function, the built-in constructor for the resource pattern. Let's see how it works.

First, we create a reactive object with the element's width and height.

const size = reactive(getSize(element));

Next, we create a ResizeObserver and observe the element.

const observer = new ResizeObserver();

observer.observe(element, () => {
  const { width, height } = getSize(element);

  size.width = width;
  size.height = height;
});

When the ResizeObserver fires, we update the width and height properties of the reactive object. Importantly, the ResizeObserver's callback runs in the Action phase, like all asynchronous callbacks invoked by the browser. This means that we can freely mutate anything in the data universe. Any part of the rendered output that cares about the reactive object will run in the next Render phase, which Starbeam will automatically schedule.

Ok, that's great, ResizeObserver requires us to disconnect from it when we no longer need it. If we don't disconnect, the observer will leak. No problem! That's the whole point of the Resource API. Let's tell Starbeam what to do when the resource is finalized.

resource.on.finalize(() => observer.disconnect());

This code is not responsible for attaching the ElementSize resource to any particular owner. That will happen inside the framework adapters, which know how to turn your framework's concept of component into a Starbeam owner.

return size;

Finally, we return the size object. The Resource function returns an object with an owner() method on it, which the caller can use to link the resource to an owner. The owner() method returns the object with reactive width and height properties.

📒 The Resource Interface
interface Resource<T> {
  owner(parent: object): T;
}

Once linked, ElementSize is a regular formula that can be used as part of other formulas.

📒 Framework-Specific Details

Starbeam's framework adapters provide a way to attach a function that takes an Element (called an "element modifier") to an element when the framework has created it using framework-specific APIs.

For example, you would use the ref API to attach a modifier in React, while you would use the use: directive syntax to attach a modifier in Svelte. Check out the framework-specific documentation for more details.

⚛️ A React Example

If all of this is getting too abstract, let's take a look at how you would actually use ElementSize in React.

function Box({ children }) {
  return useReactiveElement((element) => {
    const div = ref(HTMLDivElement);
    const size = element.useModifier(div, ElementSize);

    return () => (
      <>
        {size.match({
          rendering: () => null,
          attached: (size) => `${size.width}x${size.height}`,
        })}
        <div ref={div}>{children}</div>
      </>
    );
  });
}

@starbeam/react provides a React-specific way to create a ref to put into your JSX, and then use the ElementSize modifier with that ref. @starbeam/react takes care of interacting with React to get the element, and invokes the ElementSize modifier once the element is in the DOM.

Since the React ref API requires you to complete a full render cycle in order to attach the ref, the useModifier API in @starbeam/react returns a value that can either be rendering, because it's the first render, or attached, once the element is in the DOM. You can use the match API to decide what to do on the first render.

Critically, while the useReactiveElement, ref and useModifier APIs come from @starbeam/react, they interact with the universal ElementSize modifier that we wrote without having to know anything about React at all.

Terms

Finalize
When an object is finalized, it means that it is no longer in use, and it's safe to clean up internal references to data structures that require cleanup. Starbeam finalization refers to many concepts you may have seen in other contexts: destruction, teardown, cleanup, unmounting and deactivation.
Owner
When a finalizable object has an owner, it will be finalized when its owner is finalized.
Link
The process of assigning a finalizable object to an owner is called linking.
Resource
A pattern for creating a formula that application code can easily link to an owner

The Concept of "Lifecycle"

TODO: Describe the difference between a general concept of "lifecycle hooks", as presented by other frameworks, and how we think about the interaction between phasing and finalization in Starbeam.

FAQs

Package last updated on 20 Dec 2022

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc